How it works topic
How it works
Three descriptions of one model
The same model can be written as a Gaussian process, as a stochastic differential equation, or as a state-space model for a Kalman filter. The package computes the third, which gives the same answer as the first.
As a Gaussian process
Put a prior f ~ GP(0, k) on the signal, observe it with noise, and the
posterior is the textbook expression:
E[f(t*) | y] = k*' C^-1 y, Var[f(t*) | y] = k(t*,t*) - k*' C^-1 k*
log p(y) = -0.5 (y' C^-1 y + log|C| + N log 2 pi), C = K + sigma_eps^2 I
This is exact, and costs O(N³) time and O(N²) memory. It is the reference
the tests compare against; see Validation.
As a stochastic differential equation
For a Markovian kernel the same prior is a linear SDE. Let the rate of change be a Wiener process and the level be its integral:
d(mu) = nu dt, d(nu) = sigma dB
which discretises exactly, over any gap dt, to
A(dt) = [[1, dt], Q(dt) = sigma^2 [[dt^3/3, dt^2/2],
[0, 1]] [dt^2/2, dt ]]
The off-diagonal term in Q is what matters for irregular sampling: over a
gap, uncertainty about the slope integrates into uncertainty about the level,
and the two become correlated. A discrete local linear trend with a diagonal
Q drops that term and is exact only for unit steps, which is why time here is
a double and not an index.
As a Kalman filter and RTS smoother
These compute the same posterior and the same log marginal likelihood in O(N)
time and memory, and give the slope and the per-component decomposition along
the way.
The implied kernel for LocalLinearTrend is the cubic spline kernel
k(t,t') = σ²(m³/3 + m²|t−t'|/2) with m = min(t, t'), so the trend curve is
a natural cubic smoothing spline (Wahba 1978). The smoothing parameter is
lambda = measurementVariance / processVariance, and fit estimates it by
maximum marginal likelihood rather than by cross-validation.
Exact diffuse initialisation
A trend or a seasonal has no proper prior: the level of a random walk has no stationary distribution. The flat prior can be approximated with a very wide proper one, or the flat directions can be handled exactly. The package does the latter by default.
It uses augmentation rather than a second set of recursions. Write the
initial state as x(0) = a + B d, with d unknown and flat. Everything
downstream is affine in d, so the filter carries the sensitivity dx/dd
alongside the state (one extra mean propagation per flat direction, no extra
covariance work), and the flat directions are integrated out in closed form at
the end of the pass. The smoother reuses the same gains and recombines by the
law of total variance.
The integration leaves a log|M| term, which makes the result exactly the
restricted likelihood. It is comparable across models only when the diffuse
structure matches; see
Choosing a model.
Because it is exact, the data has to determine those directions. A two-state
trend needs readings at two distinct times; given one, the package throws an
UnderdeterminedModelException instead of returning a variance whose size
comes from the prior, and the message names the component responsible where it
can. The same goes for two components that produce the same signal,
such as a trend beside a level, or one event entered twice.
Whether M is singular is decided after scaling it to unit diagonal, so the
decision does not depend on the units of each direction: a direction is refused
when less than one part in 1e10 of its information is not already carried by
the others. A matrix that is singular in exact arithmetic leaves a pivot of
rounding size, about 1e-16, whose sign is an accident; the most
ill-conditioned determined models in the test suite stay above 1e-8.
ApproximateDiffuse uses a very wide proper prior instead. It gives every flat
direction the same prior variance whatever its units, so it only works when
time is measured in a unit that keeps rates of change near order one, such as
days for a daily series. With time in seconds or milliseconds a slope's
prior is many orders of magnitude too wide next to a level's, and the curve can
be off by a sizeable fraction of the noise with a band of zero width, whatever
kappa is. Exact initialisation does not depend on the time unit at all.
The filter
The measurement update is in Joseph form. For a scalar observation it expands
to a rank-two symmetric update costing O(n²) rather than O(n³), and it stays
symmetric and positive semi-definite for any gain, including one degraded by
rounding. The textbook P = (I − KH)P⁻ is algebraically equal but numerically
worse.
A and Q are block diagonal, so a prediction costs 2n · Σnᵢ² rather than
2n³. The covariance is dense, since the gain is a rank-one update across every
state, but the transition is not.
A two-state single-component model has a scalar fast path, with the loops
unrolled and the state in local doubles. It is a little over three times faster
than the generic engine on a forward pass. The reference tests validate the
generic engine, and fast_path_equivalence_test.dart checks that the two agree
to 1e-12.
The smoother
It solves P⁻G' = A P by Cholesky rather than forming an inverse. When a
predicted covariance is not quite positive definite it adds a small multiple of
its own mean diagonal and tries again, and after six escalations it throws a
NumericalBreakdownException. The multiple that worked is tried first at the
following steps, as a fraction of each step's scale, so a grid point far past
the data does not change the answer inside it.
It runs only over the states that can move. A coefficient with A = I and
Q = 0 under a flat prior has covariance identically zero conditional on the flat
directions, so its smoother gain has zero rows and zero columns: it smooths to
its filtered value and contributes nothing to anyone else's. Since the backward
pass is cubic in the state dimension, a trend plus twenty holiday indicators is
the difference between two states and twenty-two. Component.isStatic is how a
component declares the property.
The results overwrite the filtered moments in place. Each smoothed step is read once, by the step before it, so nothing is lost, and a decade of daily data at sixteen states is a few megabytes per array.
The fit
The measurement variance is concentrated out analytically. Scaling every
covariance in the model by a constant leaves the gains and every innovation
unchanged and scales every innovation variance by that constant, so the noise
level has a closed form given the ratios. A k-component model is therefore a
k-dimensional search rather than k + 1.
The search has three stages. A coordinate scan sweeps each parameter across its whole bracket, because a likelihood that is flat over decades, or multimodal, can leave a local search stuck. Then golden section for one parameter, or Nelder–Mead with a restart for several. Finally each parameter is probed along its own axis to see how far it can move before the objective falls by half a nat.
SearchStart.previousParameters skips the scan and starts from the model you
passed in, for refitting as data arrives. With one parameter the golden-section
window moves along for as long as the optimum lands on its edge.
A stationary component can take over the measurement noise. When a Matérn's
or a cycle's variance finishes at the top of its bracket, fit searches again
from starts that hand the noise a larger share, and keeps whichever optimum is
higher.
Data a model explains exactly, such as identical readings under a trend, profiles to a noise level of zero. The concentrated noise variance is floored at (1e-9 times the largest reading)², so the fit still returns a model.
Shape parameters measured in time get a floor from the data, because below the sampling interval they no longer describe a different model. See Components.
What the fit tells you afterwards
parameterStatus: whether each parameter was estimated, shrunk out at the bottom of its bracket, or pushed past an edge (the top, or for a shape parameter either end). A parameter counts as on a bound only when the likelihood at the bound is within half a nat of the optimum. A variance of zero is the edge of the parameter space rather than an interior point, so the width reported for such a parameter is one-sided and is not an error bar.plateauDecadesByParameter: how far each parameter can move before the fit loses half a nat.NaNwhere the coordinate is not a logarithm;plateauWidthByParameterhas the raw number.warnings: the above in sentences, including the case where a width was measured while another parameter sat on a bound.
The residuals
Standardised residuals are the recursive ones. Under a flat prior the innovation is an affine function of the unknown starting point, and substituting the final estimate would condition every residual on the whole series, including its own future. Instead the starting point is re-estimated from what came strictly before each observation, which makes the errors exactly independent under the model.
Their sum of squares reproduces the likelihood's quadratic form to 6e-11
relative, as it should: the restricted likelihood factorises into these
predictive densities. There are N − d of them, as many as the likelihood
charges for.
The penalty that is off by default
ComplexityPenalty puts a penalised-complexity penalty on the variances and is
off by default. It makes no measurable difference to the trend/seasonal
decomposition and is worse at recovering the variances; what it does reliably
is push a component's drift parameter to the floor when there is no drift. The measurements are in
Validation.
Classes
- Component Components How it works
- One additive block of a structural time-series model.
- Initialization How it works
- How the prior on the state at the first time step is specified.